/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.tnmk.worktime.service;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import org.joda.time.DateTime;
import org.joda.time.DateTimeConstants;
import org.tnmk.common.util.BeanUtils;
import org.tnmk.common.util.Filter;
import org.tnmk.worktime.service.action.Action;
import org.tnmk.worktime.service.dao.SessionDao;
import org.tnmk.worktime.service.dao.SpendDao;
import org.tnmk.worktime.service.dao.entry.SessionEntry;
import org.tnmk.worktime.service.dao.entry.SessionType;
import org.tnmk.worktime.service.dao.entry.SpendEntry;
import org.tnmk.worktime.service.model.SessionModel;
import org.tnmk.worktime.utils.TimePeriodUtils;
import org.tnmk.worktime.utils.TimeUtils;

/**
 * @since 2014-06-12
 * @author tnmkhoi This class handles: + Current WorkSession: continously update
 * workedHours & outTime + Re-calculate time/hour for any workSession in history
 * and related sessions in a week/month
 */
public class SessionService implements Runnable {

  private final Logger log = Logger.getLogger(this.getClass().getSimpleName() + "@" + this.hashCode());

  public static final float WORK_HOURS_IN_WEEK = 40;
  public static final int WORK_DAYS_IN_WEEK = 5;
  private static final int SHORT_PERIOD_HOURS = 2;
  public static final float WORK_HOURS_IN_DAY = 8;
  public static final int[] OFF_DAYS = new int[]{DateTimeConstants.SATURDAY, DateTimeConstants.SUNDAY};

  private SessionModel currentSession;
  private DateTime currentTime = DateTime.now();
  private Action outTimeNotifier;
  private Action scheduleAction;//action when running  

  private final CalculatorService calculatorService = CalculatorService.getInstance();
  SessionDao sessionDao = SessionDao.getInstance();
  SpendDao spendDao = SpendDao.getInstance();

  //<editor-fold defaultstate="collapsed" desc="Runnable">
  public void start() {
    this.initNewSession();
    ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
    scheduler.scheduleAtFixedRate(this, 0, 1, TimeUnit.SECONDS);
  }

  public void initNewSession() {
    currentSession = new SessionModel();
    currentSession.getDaoData().setInTime(currentTime);
    mergeSessionIfPossible();
    currentSession.getDaoData().setOutTime(currentTime);
    currentSession.getDaoData().setBreakBeginTime(currentTime.withTime(12, 0, 0, 0));
    currentSession.getDaoData().setBreakEndTime(currentTime.withTime(13, 30, 0, 0));
    if (CalculatorService.isOffDay(currentSession.getInTime())) {
      currentSession.setType(SessionType.Off);
    }
    sessionDao.addOrUpdate(currentSession.getDaoData());//set to get newId
    recalculateModel(currentSession);
  }

  public void mergeSessionIfPossible() {
    SessionEntry previousSession = sessionDao.findPreviousSession(currentSession.getDaoData());
    if (previousSession == null) {
      return;
    }
    if (isShortPeriod(previousSession.getOutTime(), currentSession.getDaoData().getInTime())) {
      //TODO Merge current session with previous session (include id ?). Should not merge ID
      BeanUtils.copyProperties(previousSession, currentSession.getDaoData(), new Filter() {
        @Override
        public boolean accept(Object value) {
          return (value != null);
        }
      });
      sessionDao.delete(previousSession.getId());
    }
  }

  /**
   * Never call this method directly, call start() instead.
   */
  @Override
  public void run() {
    currentTime = DateTime.now();
    currentSession.getDaoData().setOutTime(currentTime);
    recalculateModel(currentSession);//Not neccessary
    if (currentSession.isWorkDay() && currentTime.isAfter(currentSession.getDaoData().getExpectedOutTime())) {
      outTimeNotifier.perform();
    }
    this.scheduleAction.perform();
  }

  public void stop() {
    this.currentSession.getDaoData().setOutTime(DateTime.now());
    sessionDao.addOrUpdate(currentSession.getDaoData());
    saveAll();
  }

  public void updateCurrentSession() {
    sessionDao.addOrUpdate(currentSession.getDaoData());
  }

  public void saveAll() {
    sessionDao.save();
  }

  public void backupData() {
    sessionDao.backupData();
  }
  //</editor-fold>

  //<editor-fold defaultstate="collapsed" desc="Re-calculate">
  public SessionModel recalculate(SessionEntry session) {
    SessionModel model = new SessionModel(session);
    recalculateModel(model);
    return model;
  }

  /**
   * @param model recalculate both workedHours and expectOutTimes
   */
  public void recalculateModel(SessionModel model) {
    SessionEntry entry = model.getDaoData();
    List<SessionEntry> weekSessions = sessionDao.findWeekSessions(entry);
    List<SessionEntry> monthSessions = sessionDao.findMonthSessions(entry);
    recalculateModel(model, weekSessions, monthSessions);
    addOrUpdateSessionAndSpends(entry);
    //If this session is in current week, but different from current session, then recalculate current model too
    if (!entry.getId().equals(this.currentSession.getDaoData().getId()) && TimePeriodUtils.isInThisWeek(entry.getInTime())) {
      recalculateModel(this.currentSession);
    }
  }

  /**
   * only calculate workedHours, not expectOutTimes
   *
   * @param model
   * @param weekSessions
   * @param monthSessions
   */
  public void recalculateModel(SessionModel model,
          List<SessionEntry> weekSessions, List<SessionEntry> monthSessions) {
    recalculateWorkedHours(model, weekSessions, monthSessions);
    recalculateSpend(model, weekSessions, monthSessions);
    float avgRequiredHours = 0;
    if (model.isWorkDay()) {
      avgRequiredHours = calculatorService.reckonAvgRequiredHours(model.getWeekWorkedHours(), weekSessions, model.getDaoData());
    }
    currentSession.getDaoData().setExpectWorkHoursToday(avgRequiredHours);
    calculatorService.reckonOutTime(model.getDaoData());
  }

  /**
   * Recalculate:
   * <li>+ WorkedHours</li>
   * <li>+ WorkedHours in Week</li>
   * <li>+ WorkedHours in Month</li>
   *
   * @param model
   * @param weekSessions
   * @param monthSessions
   */
  protected void recalculateWorkedHours(
          SessionModel model,
          List<SessionEntry> weekSessions,
          List<SessionEntry> monthSessions) {
    calculatorService.reckonWorkedHours(model.getDaoData());
    model.setWeekWorkedHours(calculatorService.reckonWorkedHours(weekSessions));
    model.setMonthWorkedHours(calculatorService.reckonWorkedHours(monthSessions));
  }

  protected void recalculateSpend(
          SessionModel model,
          List<SessionEntry> weekSessions,
          List<SessionEntry> monthSessions) {
    model.setSumSpend(calculatorService.reckonSpend(model.getDaoData()));
    model.setSumWeekSpend(calculatorService.reckonSpend(weekSessions));
    model.setSumMonthSpend(calculatorService.reckonSpend(monthSessions));
  }
  //</editor-fold>

  public SessionEntry findSession(Integer id) {
    return sessionDao.find(id);
  }

  /**
   * The world outside communicates to service's detail via ServiceModel, not
   * DaoEntry.
   *
   * @param session
   */
  public void addOrUpdateSessionAndSpends(SessionModel session) {
    this.addOrUpdateSessionAndSpends(session.getDaoData());
  }

  public void addOrUpdateSessionAndSpends(SessionEntry session) {
    sessionDao.addOrUpdateSessionAndSpends(session);
  }

  /**
   * @param timeA
   * @param timeB
   * @return check timeA.endTime is not far from timeB.beginTime
   */
  private boolean isShortPeriod(DateTime a, DateTime b) {
    long timeA = a.getMillis();
    long timeB = b.getMillis();
    long distance = timeB - timeA;
    if (distance < 0) {
      distance = -distance;
    }
    boolean rs = (distance < TimeUtils.ONE_HOUR_MILLIS * SHORT_PERIOD_HOURS);
    return rs;
  }
  //</editor-fold>

  //<editor-fold defaultstate="collapsed" desc="Setter - Getter">
  public Action getOutTimeNotifier() {
    return outTimeNotifier;
  }

  public void setOutTimeNotifier(Action outTimeNotifier) {
    this.outTimeNotifier = outTimeNotifier;
  }

  public SessionModel getCurrentSession() {
    return currentSession;
  }

  public List<SessionEntry> loadHistory() {
    return sessionDao.findAll();
  }

  public List<SessionEntry> loadHistoryWithInTimeFilter(Filter<DateTime> dateTimeFilter) {
    List<SessionEntry> rs = new ArrayList<>();
    List<SessionEntry> allSessions = loadHistory();
    for (SessionEntry entry : allSessions) {
      DateTime inTime = entry.getInTime();
      if (dateTimeFilter.accept(inTime)) {
        rs.add(entry);
      }
    }
    return rs;
  }

  public Action getScheduleAction() {
    return scheduleAction;
  }

  public void setScheduleAction(Action scheduleAction) {
    this.scheduleAction = scheduleAction;
  }

  public DateTime getCurrentTime() {
    return currentTime;
  }

  public void setCurrentTime(DateTime currentTime) {
    this.currentTime = currentTime;
  }
  //</editor-fold>

  /**
   * @param session
   * @param spend In Buffer, Session entry will have reference to session
   */
  public void addSpend(SessionModel session, SpendEntry spend) {
    spendDao.addSpend(session.getDaoData(), spend);
  }

  /**
   *
   * @param id
   */
  public void deleteSpend(Integer spendId) {
    spendDao.delete(spendId);
  }

  public void deleteSessionAndRelatedSpends(Integer sessionId) {
    SessionEntry session = sessionDao.find(sessionId);
    List<SpendEntry> spends = session.getMoneySpend().getItems();
    for (SpendEntry spendEntry : spends) {
      spendDao.delete(spendEntry.getId());
    }
    sessionDao.delete(sessionId);
  }

  public SpendEntry findSpend(Integer id) {
    return spendDao.find(id);
  }

  public void addOrUpdateSpendAndCorrespondingSession(SpendEntry spend) {
    spendDao.addOrUpdate(spend);
    SessionEntry session = sessionDao.find(spend.getSessionId());
    int spendIndex = findSpendInSession(session, spend.getId());
    if (spendIndex >= 0) {
      session.getMoneySpend().getItems().set(spendIndex, spend);
    }
    sessionDao.addOrUpdate(session);
  }

  public static int findSpendInSession(SessionEntry session, Integer spendId) {
    if (spendId == null) {
      return -1;
    }
    int i = 0;
    List<SpendEntry> spends = session.getMoneySpend().getItems();
    for (SpendEntry spendEntry : spends) {
      if (spendId.equals(spendEntry.getId())) {
        return i;
      }
      i++;
    }
    return -1;
  }

}
